카오스몽키로 런타임환경에서 어플리케이션 장애 주입하기

Posted by Yungwang Ryu on 2022-10-18

배경

“송금 API 오류 났을때 화면을 보고 싶은데 어떻게 해요?”
“타임아웃 났을때 서비스가 어떻게 되는지 보고 싶은데 어떻게 해요?”

이런 요구사항을 받았을때, 생각나는건 특정 파라미터 하드코딩하여 if문을 추가하거나
prfofile 환경이 개발용 운영용 클래스를 분리하는 방법으로 했었다.

이러한 방식은 코드를 직접건들기에 나이스하지 않다고 생각했고 이때 스치듯 카오스몽키가 생각이 났다.

이참에 사용 방법을 예제를 통해서 정리해보고 실무에 적용하여 하드코딩하지 않고 런타임 환경에서 장애를 주입하여 유연한 테스트 및 장애 대응 훈련을 대비해 보도록 해보자

카오스몽키

카오스몽키란 넷플릭스에서 만든 장애주입이 가능한 오픈소스 라이브러이다.
넷플릭스에서 AWS상 마이크로서비스에 대해 장애를 주입시킴으로써 대응 훈련하기 위해 만들어 졌다고한다. 이밖에도 region, zone 레벨에 장애주입이 가능한 카오스콩, 카오스고릴라도 있다.

여기서는 어플리케이션 레벨에서 장애주입이 가능한 카오스몽키에 대해서 케이스별로 활용 방법을 정리해 보자

카오스몽키 동작 방식

공식 문서를 보면 AOP로 공격을 트리거 하는 방식으로 진행되며 어느 부분(spring bean)에 어떤 공격 (지연, 예외, 셧다운)을 할지 정해 주기만 하면 된다. 참고로 지연을 설정하게 되면 카오스몽키는 Thread.sleep()을 통해 지연을 시킨다.

사전 준비

먼저 카오스콩키 장애 주입 방식을 yml이 아닌 api로 하려고 한다.
yml로 설정 하면 설정값이 바뀔때 마다 서버 재시작이 필요하여 유연성이 떨어지기 때문이다.
따라서 http 요청을 편리하게 하기 위해 httpie를 설치하여 진행 하였다.

카오스몽키 적용 하기 (feat: CM4SB)

스프링부트에 적용하는 방법은 매우 간단하다.
문서에 나온것 처럼 라이브러리 의존을 추가하고 yml profile에 chaos-monkey를 추가하고 서버를 올리면 카오스몽키로고가 보이면 성공이다.

카오스몽키 API로 장애 주입 설정 팁

설정하다보면 watchers로 resctController, componet 등 여러 빈들을 설정 할 수가 있다.
만약 restController, component 설정하고 지연은 3s로 했다면 모든 watchers마다 3초씩 지연이 발생한다. 따라서 내가 지연을 주고 싶은 메소드 또는 클래스에만 watcher로 걸어 줘야 한다.

시나리오: RestController만 요청 마다 2s ~ 5s 지연 시키기

자 이제 본격적으로 카오스몽키에서 제공하는 API로 장애 설정 및 트리거를 걸어보자.
level = 1로 설정 하게 되면 요청할때 마다 지연이 발생하게 되고
level = 3으로 하게 되면 3번중 1번꼴로 지연이 발생 된다.
확인 할 때 watchedCustomService 설정이 되어 있으면 RestController 이더라도 지연이 되지 않으니 설정을 먼저 초기화 한 후에 진행해야 한다.

1
2
3
4
5
6
7
8
9
10
11
// 공격 설정 및 와쳐 확인
http localhost:8080/actuator/chaosmonkey

// 설저 초기화
http post localhost:8080/actuator/chaosmonkey/watchers rest_controller=true component=false watched_custom_services:="[]"

// 공격 주입
http POST localhost:8080/actuator/chaosmonkey/assaults level=1 latencyRangeStart=2000 latencyRangeEnd=5000 latencyActive=true

// 엔드포인 확인
http post localhost:8080/send receiverName="루피" amount=100

jmeter로 실행결과 level=3 설정시 약 3번 요청당 1번꼴로 랜덤하게 지연이 발생하고 있는데, 랜덤하게 라고 말한이유는 무조건 3번째 요청일때 지연이 발생하는것은 아니기 때문이다.

하지만 이걸로는 내가 사용해야 하는 이유에 충분치 않다. 이러면 모든 @RestController로 선언된 모든 엔드포인트가 일괄 적용 되어 원치 않는 엔드포인트도 지연이 발생 되기 때문이다. 실무에서 이러면 개발서버에서 다른 사용자들까지 영향을 주기 때문에 이정도로는 사용하기 적절하지 못하다.

시나리오: 특정 메소드에만 적용

그렇기 때문에 특정 서비스 또는 특정 메소드만 장애가 주입되도록 하면 그나마 주입 범위가 축소가 된다. 빈이 service 레이어 라면 service=true 라고 하고 특정 메소드를 지정하면 된다.
참고로 conponent, repositoy, 특정 bean, outbound에 대한 restTemplate 도 있다.

1
2
3
http post localhost:8080/actuator/chaosmonkey/watchers restController=false service=true
http post localhost:8080/actuator/chaosmonkey/assaults watchedCustomServices:='["com.chaos.monkey.SendService.send"]'
http post localhost:8080/send receiverName="루피" amount=100

이렇게 하면 지정한 메소드에서만 지연이 발생 한다.
하지만 아직도 부족하다. 장애 주입 범위는 많이 축소 되었지만 예를 들어 어떤 사람은 송금은 되게 해야 하고 QA하는 담당자는 장애 테스트가 필요하면 어떻게 해야 할까? 다시말해 특정 유저 특정 파라미터로 넘어온 경우에만 동작 시키는 방법이 없을까?

시나리오: 특정 엔드포인트에 특정 파라미터인 경우 레이턴시 적용

이전까지는 너무 넓은 범위에서 카오스몽키가 실행되었지만, 실무에서 쓰기에는 장애 주입 범위가 너무 넓어 부담스러웠다.
하지만 특정 조건에서만 카오스몽키 트리거를 걸수 있는 기능을 제공 한다. (이것이 없었으면 쓰지 않았을듯…)

파라미터 체크는 GET인 경우는 URI나 쿼리 파라미터로 하고, POST인 경우는 body에 특정 파라미터로 넘어온 경우에 카오스 몽키가 동작 하도록 하려고 한다.

POST 요청시 body를 읽기 위해 inputStream을 가져와야 하는데 이미 스프링컨테이너가 body를 객체에 맵핑하기 위해 한번 getInputStream을 하였기 때문에 body값이 항상 empty로 나오게 된다.

따라서 한번 읽어온 body 값을 wrapper로 감싸서 보관하여 필터 등록을 해야 하는데 자세한 내용은 예제를 참고 하거나 구글링을 통해서 구현하면 된다.

1
2
3
4
5
6
7
8
@Component
public class ChaosTriggerCondition implements ChaosToggles {

@Override
public boolean isEnabled(String toggleName) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
return isTrigger(request); // 트리거 되는 조건을 넣어 주면 된다.
}

이렇게 구성하면 watcher로 걸려 있더라도 특정 파라미터가 아니라면 카오스몽키가 동작하지 않게 된다!

시나리오: 특정 엔드포인트에 특정 Exception 발생시키기

특정 서비스에 특정 exception을 발생 시킬수도 있다.
어떤 exception 발생 되냐에 따라 사용자에게 보여지는 에러페이지나 로직이 달라질수 있기 때문에 이 기능도 유용하다.
라이브러리에서 제공하는 exception도 가능하고 내가 custom하게 만든 unchecked exception도 가능하다.

1
2
3
4
5
6
7
http post localhost:8080/actuator/chaosmonkey/watchers restController=false component=true
http post localhost:8080/actuator/chaosmonkey/assaults \
exceptionsActive=true \
level=1 \
exception:='{"type":"org.springframework.web.client.RestClientException"}' \
watchedCustomServices:='["com.chaos.monkey.SimpleApiAdapter"]'
http post localhost:8080/send receiverName="루피" amount=100

시나리오: outbouund로 나가는 API 클라이언트로 Timeout 발생시키기

외부 API 요청을 통해서 로직을 수행하는 경우도 있다.
이때 외부 API가 타임아웃 발생했을 때 대응해야 경우도있다. retry를 할것인지 후처리를 할것인지 말이다. 이때도 카오스몽키를 사용하면 유용하게 된다.

1
2
http post localhost:8080/actuator/chaosmonkey/watchers restTemplate=true
http POST localhost:8080/actuator/chaosmonkey/assaults level=1 latencyRangeStart=2000 latencyRangeEnd=5000 latencyActive=true

설정은 이렇게 하면 되고 참고로 카오스몽키에서 제공하고 있는 http client 외 것들은 적용이 되지 않는다.
예로 retrofit… 이럴때는 그냥 TimeoutException을 강제로 발생시켜 확인을 해야 한다.

더나아가서: 특정 파라미터는 어디서 설정 할까?

이렇게 케이스별로 카오스몽키 예제를 연습해 보았다.
실무에서 가장 사용할만한 케이스는 특정 파라미터에 특정 엔드포인트를 지연시키는 케이스 정도 될거 같은데 그럼 특정 파라미터에 대한 CRUD는 어디서 할까? 서버에 저장하면 될까? 안된다
값은 변하기도 하고 장애주입 테스트는 하루 죙일 하기 보다 테스트 할 때만 잠깐 적용하는 경우가 많기 때문이다.
그래야 애플리케이션 수정없이 runtime 환경에서 카오스몽키 트리거를 할 수 있기 때문이다.

그래서 redis에 캐쉬로 보관하면 딱이다. 로컬캐쉬는 멀티서버 환경에서 가시성 문제가 발생하기 때문에 적합지 않다.
간단하게 특정 파라미터를 CRUD 할수 있는 API를 만들고 TTL 과 같이 레디스에 넣으면 캐쉬로 존재 할때만 장애 테스트를 할 수 있게 된다.

또한 QA 할 때도 스웨거로 공유하면 QA 할때도 요긴하게 사용 할 수 있다.
물론 개발서버에서만 우선 하는게 안전하다.

애플리케이션 보안

기본적으로 카오스몽키 활성화 설정은 disabled 해놓자 얼마든지 API로 설정을 핸들링 할 수 있으니 필요 할 때 설정 하면 되기 때문이다. 또한 profile 구성을 개발서버에서만 동작 하도록 하고 카오스몽키 액츄에이터 엔드포인트도 노출되서는 안되게 해야 한다.